﻿

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.VisualBasic;
using Microsoft.CSharp;

/// <summary>
/// Specifies values that define what action to take when a query with the same name is encountered in the target project.
/// </summary>
public enum DupAction
{
    OverWrite = 0,  //the existing query is overwritten
    RenameNew = 1,  //the existing query is preserved, the new query gets a new name
    RenameOld = 2,  //the existing query is renamed, the new query is copied with the name of the existing query
    Skip = 3,       //the copy of the query is skipped
    Illegal = 4     //an illegal value was specified on the command line
}

/// <summary>
/// Specifies values that define the outcome of a CopyQuery call.
/// </summary>
public enum ImpportResult
{
    Success = 1,                    //copy was successful
    Skipped = 2,                    //copy was skipped
    Exception = 3,                  //exception occurred 
    MissingAreasOrIterations = 4,   //target project has missing classification nodes that are present in source project and referenced in the query that failed
    InvalidOnDup = 5                //invalid ondup action was specified and a duplicate query was encountered
}

namespace Qetza.TeamFoundation.WorkItemQuery.Admin
{
    /// <summary>
    /// Copies queries from a source project and collection to a target project and collection.
    /// <para></para><para></para>
    /// Can copy queries across collections, such as a test collection to a production collection.
    /// <para></para><para></para>
    /// Can copy recursively, using the /recursive switch, from the specified query folder, which can be the the root, "Team Queries", or some folder under it, 
    /// like "Change Management".
    /// <para></para><para></para>
    /// If folders in the source are not present in the target, they are created in the target as they are encountered. This means 
    /// that if an entire tree of folders are not present, they will be created from the top down.
    /// <para></para><para></para>
    /// If a query references and area or iteration node that is not present in the target project, that query will fail 
    /// but the program continues if more queries are specified for the copy.
    /// <para></para><para></para>
    /// Copy command added by J. Mike Zserai, 2012
    /// </summary>
    class CommandCopyWiq : ICommand
    {
        #region Constants

        static readonly string SourceCollectionArgumentName;
        static readonly string TargetCollectionArgumentName;
        static readonly string SourceProjectArgumentName;
        static readonly string TargetProjectArgumentName;
        static readonly string OnDupArgumentName;
        static readonly string SilentArgumentName;
        
        #endregion

        #region Constructors

        static CommandCopyWiq()
        {
            SourceCollectionArgumentName = "/collectionFrom";
            TargetCollectionArgumentName = "/collectionTo";
            SourceProjectArgumentName = "/projectFrom";
            TargetProjectArgumentName = "/projectTo";
            OnDupArgumentName = "/ondup";
            SilentArgumentName = "/silent";
        }

        #endregion

        #region ICommand Members

        private String SourceProjectName;
        private String TargetProjectName;
        private String SourceCollection;
        private String TargetCollection;
        private QueryHierarchy QueryImporter;
        private QueryHierarchy HierarchySource;
        private DupAction OnDupAction;
        private bool DoPrompt;
        private bool NoPath;
        private bool IsRecursive;
        private bool IsSilent;
        private String Folder;
        private String QueryPath;

        //these counts are included in the output to notify the user of any issues
        private Int32 QueryCount;
        private Int32 SkippedCount;
        private Int32 MissingAreaIterationCount;
        private Int32 ExceptionCount;
        private Int32 InvalidOnDupCount;
        
        /// <inheritdoc />
        public string Name
        {
            get { return "copy"; }
        }

        /// <inheritdoc />
        public string HelpText
        {
            get { return ResourceStrings.CopyWiqHelpText; }
        }

        /// <inheritdoc />
        public IEnumerable<ParameterInfo> AllowedParameters
        {
            get
            {
                return new[] {
                    new ParameterInfo() { 
                        Name = CommandCopyWiq.SourceCollectionArgumentName, 
                        ArgumentName = ResourceStrings.ProjectCollectionArgument, 
                        IsRequired = true, 
                        HelpText = ResourceStrings.CollectionSourceArgumentHelpText
                        },
                    new ParameterInfo() { 
                        Name = CommandCopyWiq.TargetCollectionArgumentName, 
                        ArgumentName = ResourceStrings.ProjectCollectionArgument, 
                        IsRequired = true, 
                        HelpText = ResourceStrings.CollectionTargetArgumentHelpText
                        },
                    new ParameterInfo() { 
                        Name = CommandCopyWiq.SourceProjectArgumentName, 
                        ArgumentName = ResourceStrings.ProjectArgument, 
                        IsRequired = true, 
                        HelpText = ResourceStrings.ProjectSourceArgumentHelpText
                        }, 
                    new ParameterInfo() {
                        Name = CommandCopyWiq.TargetProjectArgumentName,
                        ArgumentName = ResourceStrings.ProjectArgument,
                        IsRequired = true, 
                        HelpText = ResourceStrings.ProjectTargetArgumentHelpText
                        },
                    new ParameterInfo() {
                        Name = CoreArgumentNames.Recursive,
                        IncompatibleArguments = new [] { CoreArgumentNames.QueryPath },
                        IsFlag = true,
                        HelpText = ResourceStrings.RecursiveHelpText
                        },
                    new ParameterInfo() {
                        Name = CoreArgumentNames.Folder,
                        ArgumentName = ResourceStrings.FolderArgument,
                        IncompatibleArguments = new [] { CoreArgumentNames.QueryPath },
                        HelpText = ResourceStrings.Copy_FolderHelpText
                        },
                    new ParameterInfo() {
                        Name = CoreArgumentNames.QueryPath,
                        ArgumentName = ResourceStrings.QueryPathArgument,
                        IncompatibleArguments = new [] { CoreArgumentNames.Recursive },
                        HelpText = ResourceStrings.Copy_QueryPathHelpText
                        },
                    new ParameterInfo() {
                        Name = CoreArgumentNames.NoPrompt,
                        ArgumentName = ResourceStrings.NoPromptArgument,
                        IsFlag = true,
                        HelpText = ResourceStrings.NoPromptHelpText
                        },
                    new ParameterInfo() {
                        Name = CommandCopyWiq.OnDupArgumentName,
                        ArgumentName = ResourceStrings.OnDupArgument,
                        AllowedValues = new [] {"O", "RN", "RO", "S" },
                        HelpText = ResourceStrings.OnDupArgumentHelpText
                        },
                    new ParameterInfo() {
                        Name = CoreArgumentNames.NoPath,
                        ArgumentName = ResourceStrings.NoPathArgument,
                        IsFlag = true,
                        HelpText = ResourceStrings.NoPathHelpText
                        },
                    new ParameterInfo() {
                        Name = CommandCopyWiq.SilentArgumentName,
                        IsFlag = true,
                        HelpText = ResourceStrings.SilentArgumentHelpText
                        }
                };
            }
        }

        /// <inheritdoc />
        public void Execute(IDictionary<string, string> parameters)
        {
            
            QueryCount = 0;
            SourceProjectName = parameters[CommandCopyWiq.SourceProjectArgumentName].ToUpper();
            TargetProjectName = parameters[CommandCopyWiq.TargetProjectArgumentName].ToUpper();
            SourceCollection = parameters[CommandCopyWiq.SourceCollectionArgumentName].ToUpper();
            TargetCollection = parameters[CommandCopyWiq.TargetCollectionArgumentName].ToUpper();
            DoPrompt = parameters.ContainsKey(CoreArgumentNames.NoPrompt);
            IsRecursive = parameters.ContainsKey(CoreArgumentNames.Recursive);            
            NoPath = parameters.ContainsKey(CoreArgumentNames.NoPath);
            IsSilent = parameters.ContainsKey(CommandCopyWiq.SilentArgumentName);

            if (parameters.ContainsKey(CommandCopyWiq.OnDupArgumentName))
            {
                OnDupAction = ConvertAction(parameters[CommandCopyWiq.OnDupArgumentName]);
            }
            else
            {
                OnDupAction = ConvertAction(ResourceStrings.DefaultOnDup);
            }
            
            if (parameters.ContainsKey(CoreArgumentNames.Folder))
            {
                Folder = parameters[CoreArgumentNames.Folder];
            }

            if (parameters.ContainsKey(CoreArgumentNames.QueryPath))
            {
                QueryPath = parameters[CoreArgumentNames.QueryPath];
            }

            // get query hierarchy - Source
            HierarchySource = TfsUtility
                .GetWorkItemStore(SourceCollection, DoPrompt)
                .Projects[SourceProjectName]
                .QueryHierarchy;

            // get query hierarchy - Target
            QueryImporter = TfsUtility
              .GetWorkItemStore(TargetCollection, DoPrompt)
              .Projects[TargetProjectName]
              .QueryHierarchy;

            var rootSource = (QueryFolder)HierarchySource;
            var parentPathSource = string.Empty;

            if (!NoPath)
            {
                parentPathSource = Folder + "/";
            }
 
            // get root folder if specified
            if (parameters.ContainsKey(CoreArgumentNames.Folder))
            {
                var item = HierarchySource.Find(Folder);
                if (item == null)
                {
                    throw new Exception(string.Format(ResourceStrings.ErrorUnknownFolder, Folder));
                }

                if (!(item is QueryFolder))
                {
                    throw new Exception(string.Format(ResourceStrings.ErrorNotAFolder, Folder));
                }

                rootSource = (QueryFolder)item;
                // copy the source queries to the target
                // recursive - all queries AND folders/queries in rootSource, else just the queries in rootSource
                CopyAllQueries(rootSource, parentPathSource, IsRecursive, NoPath, SourceProjectName);
            }
            else
            {
                //single query copy - or the query name itself
                if (parameters.ContainsKey(CoreArgumentNames.QueryPath))
                {
                    //find the source query
                    var item = HierarchySource.Find(QueryPath);
                    if (item is QueryDefinition)
                    {
                        String targetFolder;
                        XDocument xml;
                        if (CreateNewQuery((QueryDefinition)item, SourceProjectName, out targetFolder, out xml))
                        {
                            //copy current query to the correct folder
                            switch (CopyQuery(xml, targetFolder, parentPathSource, OnDupAction))
                            {
                                    //track counts to report to user
                                case ImpportResult.Success:
                                    {
                                        QueryCount++;
                                        break;
                                    }
                                case ImpportResult.Exception:
                                    {
                                        ExceptionCount++;
                                        break;
                                    }
                                case ImpportResult.InvalidOnDup:
                                    {
                                        InvalidOnDupCount++;
                                        break;
                                    }
                                case ImpportResult.MissingAreasOrIterations:
                                    {
                                        MissingAreaIterationCount++;
                                        break;
                                    }
                                case ImpportResult.Skipped:
                                    {
                                        SkippedCount++;
                                        break;
                                    }
                            }
                        }
                    }
                }
            }

            // done!
            if ((MissingAreaIterationCount + ExceptionCount + SkippedCount + InvalidOnDupCount) > 0)
            {
                //errors encountered, so tell user
                if (!IsSilent)
                {
                    Console.WriteLine(String.Format(ResourceStrings.CopyWiqCompleted,
                                                    QueryCount.ToString(),
                                                    MissingAreaIterationCount.ToString(),
                                                    ExceptionCount.ToString(),
                                                    SkippedCount.ToString(),
                                                    InvalidOnDupCount.ToString()));
                }
            }
            else
            {
                //no errors or anomalies, so report query copy count and leave
                if (!IsSilent) { Console.WriteLine(String.Format(ResourceStrings.CopyWiqCompleted, QueryCount.ToString())); }            
            }
        }

        /// <inheritdoc />
        public void ValidateParameters(IDictionary<string, string> parameters)
        {
            // validate that source and target aren't the same
            if (parameters[CommandCopyWiq.SourceCollectionArgumentName] == parameters[CommandCopyWiq.TargetCollectionArgumentName] &&
                parameters[CommandCopyWiq.SourceProjectArgumentName] == parameters[CommandCopyWiq.TargetProjectArgumentName])
            {
                throw new InvalidParameterException(
                    string.Format(ResourceStrings.ErrorSourceMustBeDifferentThanTarget)
                    );
            }

            // validate one or the other of /folder or /querypath parameter specified - mutually exclusive
            if ((parameters.ContainsKey(CoreArgumentNames.Folder) && parameters.ContainsKey(CoreArgumentNames.QueryPath)) || 
                (!parameters.ContainsKey(CoreArgumentNames.Folder) && !parameters.ContainsKey(CoreArgumentNames.QueryPath)))
            {
                throw new InvalidParameterException(
                    string.Format(ResourceStrings.ErrorExactlyOneMustBeSpecified, CoreArgumentNames.Folder + "," + CoreArgumentNames.QueryPath)
                    );
            }

            return;
        }

        #endregion

        #region Private Helpers

        /// <summary>
        /// Copies one or more work item queries from one collection and project to another 
        /// collection and project. if recursion is specified, all queries under the specified folder 
        /// are imported.
        /// </summary>
        /// <param name="folder">The source folder to copy queries from.</param>
        /// <param name="parentPath">The source folder's parent path.</param>
        /// <param name="isRecursive">True to copy all queries recursively under the specified folder, and 
        /// folders under it. Otherwise, just copy the queries under the specified folder.</param>
        /// <param name="noPath">Specifies whether to write just the query name on the console after each import 
        /// action or the fully-qualified path.</param>
        /// <param name="sourceProject">The name of the source team project. Used to remove the source project name from the target folder path.</param>
        private void CopyAllQueries(QueryFolder folder, string parentPath, bool isRecursive, bool noPath, String sourceProject)
        {
            foreach (var item in folder)
            {
                if (item is QueryDefinition)                
                {
                    String targetFolder;
                    XDocument newQuery;
                    if (CreateNewQuery((QueryDefinition)item, sourceProject, out targetFolder, out newQuery))
                    {
                        //copy current query to the correct folder
                        switch (CopyQuery(newQuery, targetFolder, parentPath, OnDupAction))
                        {
                            case ImpportResult.Success:
                                {
                                    QueryCount++;
                                    break;
                                }
                            case ImpportResult.Exception:
                                {
                                    ExceptionCount++;
                                    break;
                                }
                            case ImpportResult.InvalidOnDup:
                                {
                                    InvalidOnDupCount++;
                                    break;
                                }
                            case ImpportResult.MissingAreasOrIterations:
                                {
                                    MissingAreaIterationCount++;
                                    break;
                                }
                            case ImpportResult.Skipped:
                                {
                                    SkippedCount++;
                                    break;
                                }
                        }
                    }
                }
                else if (isRecursive)
                {
                    //check in target project QueryHiearchy for this folder and create it if not found
                    //this is necessary because multiple folders in the path to the current query may not exist in the target project
                    //so we have to create them as we recurse over them until we get to the queries to copy.
                    //If we don't and we try to create the last folder and it's parent does not exist, it will fail.

                    // make path anonymous by removing source project name, maintain case
                    String targetFolder = item.Path;
                    if (targetFolder.ToUpper().Contains(sourceProject))
                    {
                        targetFolder = targetFolder.Substring((sourceProject + "/").Length);
                    }

                    //creates the folder only if not found, otherwise returns a reference to existing folder
                    var folderToCreate = QueryImporter.CreateFolder(targetFolder);
                    QueryImporter.Save();

                    this.CopyAllQueries((QueryFolder)item, parentPath + item.Name + "/", isRecursive, noPath, sourceProject);
                }
            }
        }

        /// <summary>
        /// Imports the specified Work Item Query (WIQ) into the specified target folder, project, and collection. If the query 
        /// already exists, it takes the specified action (overwrite, rename, skip).
        /// </summary>
        /// <param name="newQuery">The work item query to import.</param>
        /// <param name="targetFolder">The folder to place the imported query.</param>
        /// <param name="parentPath">The folder's parent path.</param>
        /// <param name="onDup">The <see cref="DupAction"/> to take if the named query already exists.</param>
        public ImpportResult CopyQuery(XDocument newQuery, String targetFolder, String parentPath, DupAction onDup)
        {
            //defined here so we can reference it in the exception handler section
            QueryItem query = null;
            ImpportResult result = ImpportResult.Success;

            try
            {
                var doc = new XDocument(newQuery);
                var queryText = doc.ToString();

                // get or create query folder
                var index = targetFolder.LastIndexOf('/');
                var folderPath = (index == -1 ? string.Empty : targetFolder.Substring(0, index));
                var queryName = targetFolder.Substring(index + 1);

                var folder = QueryImporter.CreateFolder(folderPath);

                // create or update query
                query = folder.Find(queryName);

                if (query == null)
                {

                    // new query in the target project, so no OnDup action here
                    // normalize the query text to pass to constructor
                    queryText = NormalizeQuery(queryText);
                    queryText = queryText.Replace(HierarchySource.Project.Name, QueryImporter.Project.Name);
                    query = new QueryDefinition(queryName, queryText, folder);

                    QueryImporter.Save();
                    if (!IsSilent) { Console.WriteLine(("Copied " + (NoPath ? string.Empty : query.Parent.ToString() + "/") + query.Name)); }
                }
                else //the query exists in the target project already
                {
                    if (!(query is QueryDefinition))
                    {
                        // item is not a query
                        throw new Exception(string.Format(ResourceStrings.ErrorFolderAlreadyExists, folder));
                    }
                    else
                    {
                        if (onDup != DupAction.Skip && onDup != DupAction.Illegal)
                        {
                            // update query, but only if we'll be using it
                            // normalize the query text to save
                            queryText = NormalizeQuery(queryText);
                        }

                        switch (onDup)
                        {
                            case DupAction.OverWrite:
                                ((QueryDefinition)query).QueryText = queryText;
                                QueryImporter.Save();
                                if (!IsSilent) { Console.WriteLine((ResourceStrings.OnDupOverwrittenMessage + (NoPath ? string.Empty : parentPath) + query.Name)); }
                                break;

                            case DupAction.RenameNew:
                                //create a sufficiently unique name
                                var renamedQueryNew = new QueryDefinition(GetNewQueryName(queryName, "renameNew"), queryText, folder);
                                QueryImporter.Save();
                                if (!IsSilent) { Console.WriteLine((ResourceStrings.OnDupRenameNewMessage + (NoPath ? string.Empty : parentPath) + renamedQueryNew.Name)); }
                                break;

                            case DupAction.RenameOld:
                                //create a sufficiently unique name
                                String renamedQueryOld = GetNewQueryName(((QueryDefinition)query).Name, "renameOld");
                                ((QueryDefinition)query).Name = renamedQueryOld;

                                var newQueryDef = new QueryDefinition(queryName, queryText, folder);
                                QueryImporter.Save();
                                if (!IsSilent) { Console.WriteLine((ResourceStrings.OnDupRenameOldMessage + (NoPath ? string.Empty : parentPath) + newQueryDef.Name)); }
                                break;

                            case DupAction.Skip:
                                result = ImpportResult.Skipped;
                                if (!IsSilent) { Console.WriteLine((ResourceStrings.OnDupRenameSkipMessage + (NoPath ? string.Empty : parentPath) + query.Name)); }
                                break;

                            default: // should never get here, but good practice to have default
                                result = ImpportResult.InvalidOnDup;
                                if (!IsSilent) { Console.WriteLine(ResourceStrings.IllegalDupActionMessage + ResourceStrings.IllegalDupActionHelpText); }
                                break;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                string msg;
                
                //check for the most common error
                if (ex.Message.Contains(ResourceStrings.MissingClassificationNodeErrorCode))
                {
                    result = ImpportResult.MissingAreasOrIterations;
                    msg =  ResourceStrings.MissingClassificationNodeMessage + Environment.NewLine + ex.Message;
                }
                else
                { 
                    result = ImpportResult.Exception;
                    msg = ex.Message; 
                }
                //error, report to user regardless of IsSilent switch
                Utility.WriteWrappedLines(String.Format(ResourceStrings.CopyErrorMessage + msg, targetFolder));
            }

            return result;
        }

        /// <summary>
        /// Converts the OnDup action specified on the command line to the appropriate enumeration value.
        /// </summary>
        /// <param name="onDup">The OnDup action specified on the command line to get converted.</param>
        /// <returns>A <see cref="DupAction"/> value representing the specified command line value.</returns>
        private DupAction ConvertAction(String onDup)
        {
            switch (onDup.ToUpper())
            {
                case "O":
                    return DupAction.OverWrite;

                case "RN":
                    return DupAction.RenameNew;

                case "RO":
                    return DupAction.RenameOld;

                case "S":
                    return DupAction.Skip;

                default:
                    return DupAction.Illegal;
            }
        }

        /// <summary>
        /// Prepares a query for copy.<para></para><para></para>
        /// 
        /// </summary>
        /// <param name="item">The source query.</param>
        /// <param name="sourceProject">The name of the source team project. Used to remove the project name from the target folder.</param>
        /// <param name="targetFolder">The target query folder passed back to caller.</param>
        /// <param name="xml">The actual XDocument XML of the query, passed back to caller.</param>
        /// <returns>True if successful. Otherwise False.</returns>
        private bool CreateNewQuery(QueryDefinition item, String sourceProject, out String targetFolder, out XDocument xml)
        {
            // get the source query
            xml = new XDocument(new XDeclaration("1.0", "utf-8", null), 
                  new XElement("WorkItemQuery",
                  new XAttribute("Version", "1"),
                  new XElement("Wiql", item.QueryText)));

            targetFolder = item.Path;

            // make path anonymous, maintain case
            if (targetFolder.ToUpper().Contains(sourceProject))
            {
                targetFolder = targetFolder.Substring((sourceProject + "/").Length);
            }

            return true;
        }

        /// <summary>
        /// Removes the XML markup to prepare the query to save.
        /// </summary>
        /// <param name="queryText">The query contents to normalize.</param>
        /// <returns>The normalized query text.</returns>
        private String NormalizeQuery(String queryText)
        {
            queryText = queryText.Substring(queryText.IndexOf("<Wiql>") + 6);
            queryText = queryText.Replace("</Wiql>", "").Replace("</WorkItemQuery>", "");
            queryText = queryText.Replace("&lt;", "<").Replace("&gt;", ">");
            
            return queryText;
        }

        /// <summary>
        /// Creates a new query name with the current DateTime with illegal characters removed, with a custom String tag included.
        /// </summary>
        /// <param name="currentName">The current query name.</param>
        /// <param name="version">Usually used to identify which version of the query the new name will pertain to.</param>
        /// <returns>The new query name.</returns>
        private String GetNewQueryName(String currentName, String version)
        {
            return (currentName + "_DUP_" + version + "-" + 
                    DateTime.Now.ToShortDateString().Replace("/", "-").Replace(".", "-") + "-" +
                    DateTime.Now.ToLongTimeString().Replace(":", "-").Replace(".", "-"));
        }

        #endregion
    }

    
}
